package global;

import iterator.CondExpr;
import iterator.FileScan;
import iterator.FileScanException;
import iterator.FldSpec;
import iterator.InvalidRelation;
import iterator.JoinsException;
import iterator.PredEvalException;
import iterator.RelSpec;
import iterator.TupleUtilsException;
import iterator.UnknowAttrType;
import iterator.WrongPermat;

import java.io.IOException;

import bufmgr.PageNotReadException;
import heap.FieldNumberOutOfBoundException;
import heap.HFBufMgrException;
import heap.HFDiskMgrException;
import heap.HFException;
import heap.Heapfile;
import heap.InvalidSlotNumberException;
import heap.InvalidTupleSizeException;
import heap.InvalidTypeException;
import heap.Tuple;

public class Schema {
	
	public static final short SCHEMA_STRING_LENGTH = 256;
	public static final String SCHEMA_FILE_NAME = "table_schemas";
	
	private static final AttrType[] SCHEMA_ATTR_TYPE = new AttrType[] {new AttrType(AttrType.attrString), new AttrType(AttrType.attrString), new AttrType(AttrType.attrInteger)};
	private static final short[] SCHEMA_ATTR_SIZE = new short[] {SCHEMA_STRING_LENGTH, SCHEMA_STRING_LENGTH};

	public String dbName;
	public AttrType[] attrType;
	public short[] attrSize;
	public int _bits;
	
	public Schema(String dbName, AttrType[] attrType, short[] attrSize, int bits) {
		this.dbName = dbName;
		this.attrSize = attrSize;
		this.attrType = attrType;
		this._bits = bits;
	}
	
	// Function that inserts the schema1 into the database if it doesn't already exist. If it does exist,
	// exit silently.
	
	public static void insertSchema(String dbName, AttrType[] attrType, int bits) throws InvalidSlotNumberException, Exception {
		Heapfile heapfile = new Heapfile(SCHEMA_FILE_NAME); // This is here to create the heapfile if it doesn't already exist
		
		Tuple schemaTuple = getSchemaTuple(dbName);
		if (schemaTuple != null) {
			// Schema already exists! Just return silently
			return;
		}
		
		// Schema does not exist yet. Create it!
		schemaTuple = formatSchemaTuple(dbName, attrType, bits);
		heapfile.insertRecord(schemaTuple.getTupleByteArray());
		
		// Changes have been made to the table. Flush the pages back to disk to ensure they live on
		SystemDefs.JavabaseBM.flushAllPages();
	}
	
	// Grabs the schema1 object as defined in the database for the given database name
	
	public static Schema getSchema(String dbName) throws HFException, HFBufMgrException, HFDiskMgrException, IOException, InvalidTypeException, InvalidTupleSizeException, FileScanException, TupleUtilsException, InvalidRelation, JoinsException, PageNotReadException, PredEvalException, UnknowAttrType, FieldNumberOutOfBoundException, WrongPermat {
		Heapfile heapfile = new Heapfile(SCHEMA_FILE_NAME); // This is here to create the heapfile if it doesn't already exist
		
		Tuple schemaTuple = getSchemaTuple(dbName);
		if (schemaTuple == null) {
			// Tuple was not found - return null
			return null;
		}
		
		schemaTuple.setHdr((short) 3, SCHEMA_ATTR_TYPE, SCHEMA_ATTR_SIZE);
		String attributeString = schemaTuple.getStrFld(2);
		int bits = schemaTuple.getIntFld(3);
		
		AttrType[] attributes = getAttributeArray(attributeString);
		short[] attributeSizes = getAttributeSizes(attributes);
		
		return new Schema(dbName, attributes, attributeSizes, bits);
	}
	
	// Goes through the set of attributes for the database schema1 and calculates the string sizes
	// for the string attributes
	
	private static short[] getAttributeSizes(AttrType[] attributes) {
		int stringCount = 0;
		for (int i = 0; i < attributes.length; i++) {
			if (attributes[i].attrType == AttrType.attrString) {
				stringCount++;
			}
		}
		
		// All Strings are given the same attribute size
		short[] attributeSizes = new short[stringCount];
		for (int i = 0; i < stringCount; i++) {
			attributeSizes[i] = SCHEMA_STRING_LENGTH;
		}
		
		return attributeSizes;
	}
	
	// Takes an attribute string from the database and creates the corresponding array of AttrTypes
	// to return back to the Schema object
	
	private static AttrType[] getAttributeArray(String attributeString) {
		String[] attributeStrings = attributeString.split(",");
		AttrType[] attributes = new AttrType[attributeStrings.length];
		for (int i = 0; i < attributeStrings.length; i++) {
			int attributeInt = Integer.parseInt(attributeStrings[i]);
			AttrType attribute = new AttrType(attributeInt);
			attributes[i] = attribute;
		}
		return attributes;
	}
	
	// Scans the schema1 file to get the schema1 from the database
	
	private static Tuple getSchemaTuple(String dbName) throws FileScanException, TupleUtilsException, InvalidRelation, IOException, JoinsException, InvalidTupleSizeException, InvalidTypeException, PageNotReadException, PredEvalException, UnknowAttrType, FieldNumberOutOfBoundException, WrongPermat {
		FileScan fscan = new FileScan(SCHEMA_FILE_NAME, SCHEMA_ATTR_TYPE, SCHEMA_ATTR_SIZE, (short) 3, 3, getProjList(), getCondExpr(dbName));
		Tuple returnTuple = fscan.get_next();
        fscan.close();
        return returnTuple;
	}
	
	// Formats the condition expression to be used in the file scan to find the schema1 in the database
	
	private static CondExpr[] getCondExpr(String dbName) {
		CondExpr[] expr = new CondExpr[2];
		expr[0] = new CondExpr();
		expr[0].next  = null;
		expr[0].op    = new AttrOperator(AttrOperator.aopEQ);
		expr[0].type1 = new AttrType(AttrType.attrSymbol);
		expr[0].type2 = new AttrType(AttrType.attrString);
		expr[0].operand1.symbol = new FldSpec (new RelSpec(RelSpec.outer),1);
		expr[0].operand2.string = dbName;
		expr[1] = null;
		return expr;
	}
	
	// Formats the projection list to be used in the file scan to find the schema1 in the database
	
	private static FldSpec[] getProjList() {
		FldSpec[] projlist = new FldSpec[3];
		RelSpec rel = new RelSpec(RelSpec.outer); 
		projlist[0] = new FldSpec(rel, 1);
		projlist[1] = new FldSpec(rel, 2);
		projlist[2] = new FldSpec(rel, 3);
		return projlist;
	}
	
	// Formats the schema1 tuple to be inserted into the database
	
	private static Tuple formatSchemaTuple(String dbName, AttrType[] attrType, int bits) throws InvalidTypeException, InvalidTupleSizeException, IOException, FieldNumberOutOfBoundException {
		Tuple tuple = new Tuple();
		tuple.setHdr((short) 3, SCHEMA_ATTR_TYPE, SCHEMA_ATTR_SIZE);
		tuple.setStrFld(1, dbName);
		tuple.setStrFld(2, getAttributeString(attrType));
		tuple.setIntFld(3, bits);
		return tuple;
	}
	
	// Takes an array of AttrTypes for a schema1 and converts it into a string to be stored in the database
	
	private static String getAttributeString(AttrType[] attrType) {
		String attributes = "";
		for (int i = 0; i < attrType.length; i++) {
			int attr = attrType[i].attrType;
			if (!attributes.equals("")) {
				attributes += ","; 
			}
			attributes += attr;
		}
		return attributes;
	}
	
	// Takes a schema1 and converts it to a string for display purposes
	
	public String toString() {
		String result = "DB Name: " + this.dbName + "\n";
		result += "Attribute types: ";
		for (int i = 0; i < this.attrType.length; i++) {
			result += this.attrType[i].toString() + " ";
		}
		result += "\n";
		result += "Attribute sizes: ";
		for (int i = 0; i < this.attrSize.length; i++) {
			result += this.attrSize[i] + " ";
		}
		result += "\n";
		result += String.format("Number of bits per dimension: %d", this._bits);
		return result;
	}
	
}
